﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using CloudShot.Core.Interfaces;
using CloudShot.Core.Types;
using DropNet;
using DropNet.Exceptions;
using DropNet.Models;
using RestSharp.Deserializers;

namespace DropboxImageStorage
{
  public class DropboxStorage : IImageStorage
  {
    private const string FOLDER_TO_SAVE = @"/";
    private readonly DropboxStorageSettingsControl _settingsControl;

    private CancellationTokenSource _cancellationTokenSource;
    private IConfigurationSection _configuration;

    public ISettingsControl SettingsControl
    {
      get { return _settingsControl; }
    }

    public DropboxStorage()
    {
      _settingsControl = new DropboxStorageSettingsControl(this);
    }

    public void Init(IConfigurationSection configuration)
    {
      _configuration = configuration;
      _settingsControl.Configuration = configuration;
    }

    public async Task<string> SaveImage(byte[] data, string name, bool isVideo)
    {
      if (await ValidateAuthorization())
      {
        var settings = _configuration.GetOrCreateAs<DropboxSettings>();

        bool shortenUrls = !isVideo && settings.ShortenUrls;
        bool useDirectlink = isVideo || settings.UseDirectLink;
        return await SaveFileInternal(data, name, shortenUrls, useDirectlink);
      }

      return null;
    }

    public async Task<bool> SignIn()
    {
      if (string.IsNullOrEmpty(DropboxTokens.ApplicationKey))
        throw new NullReferenceException("ApplicationKey not set.");

      if (string.IsNullOrEmpty(DropboxTokens.ApplicationSecret))
        throw new NullReferenceException("ApplicationSecret not set.");

      try
      {
        var client = new DropNetClient(DropboxTokens.ApplicationKey, DropboxTokens.ApplicationSecret);
        string authUrl = client.GetTokenAndBuildUrl();

        //open auth url in default web browser
        Process.Start(authUrl);

        if (_cancellationTokenSource != null)
          _cancellationTokenSource.Dispose();

        _cancellationTokenSource = new CancellationTokenSource();

        CancellationToken cancellationToken = _cancellationTokenSource.Token;
        UserLogin userLogin = await Task.Run(async () => await GetAccessToken(client, cancellationToken), cancellationToken);
        if (userLogin == null)
          return false;

        var settings = _configuration.GetOrCreateAs<DropboxSettings>();
        settings.Token = new TokenStorage(userLogin.Token, userLogin.Secret);
        _configuration.SaveAs(settings);

        return true;
      }
      catch (DropboxException ex)
      {
        throw ProcessDropboxException(ex);
      }
    }

    public void SignOut()
    {
      if (_cancellationTokenSource != null)
        _cancellationTokenSource.Cancel();

      var settings = _configuration.GetOrCreateAs<DropboxSettings>();
      settings.Token = null;
      _configuration.SaveAs(settings);
    }

    private static async Task<UserLogin> GetAccessToken(DropNetClient client, CancellationToken cancellationToken)
    {
      const int waitTime = 2000;
      const int repetitions = 30; //it is just 1 minute

      UserLogin accessToken = null;
      //https://www.dropbox.com/developers/start/authentication#python
      //We don't want anyone other than the user who requested access using this URL, 
      //which is why the request tokens are only live for five minutes. 
      //If the user sits on this link for greater than five minutes, 
      //we'll politely request they return to your app to try again.
      for (int i = 0; i < repetitions; ++i)
      {
        if (cancellationToken.IsCancellationRequested)
          return null;

        try
        {
          await Task.Delay(waitTime, cancellationToken);

          accessToken = client.GetAccessToken();
          if (accessToken != null)
            break;
        }
        catch (DropboxException) {}
        catch (TaskCanceledException) {}
        catch (ObjectDisposedException) {}
      }

      return accessToken;
    }

    private static Exception ProcessDropboxException(DropboxException ex)
    {
      if (ex.Response.ErrorException != null)
        return ex.Response.ErrorException;

      if (!string.IsNullOrEmpty(ex.Response.Content))
      {
        var deserializer = new JsonDeserializer();
        var jsonResponse = deserializer.Deserialize<Dictionary<string, string>>(ex.Response);

        string error = null;

        if (jsonResponse.TryGetValue("error", out error))
        {
          //NOTE: do not add inner exception, because Application.ThreadException will cut external exception, and error will not be shown
          return new Exception(error);
        }
      }

      return ex;
    }

    private async Task<bool> ValidateAuthorization()
    {
      var settings = _configuration.GetOrCreateAs<DropboxSettings>();

      if (settings.Token != null)
      {
        return true;
      }

      DialogResult dialogResult = MessageBox.Show(LocalResources.AuthorizationQuestion, Application.ProductName, MessageBoxButtons.YesNo,
                                                  MessageBoxIcon.Information);

      if (dialogResult != DialogResult.Yes)
        return false;

      return await SignIn();
    }

    private async Task<string> SaveFileInternal(byte[] data, string fullName, bool shortUrl, bool useDirectlink)
    {
      const int repetitions = 5;
      const int waitTime = 200;

      if (string.IsNullOrEmpty(DropboxTokens.ApplicationKey))
        throw new NullReferenceException("ApplicationKey not set.");

      if (string.IsNullOrEmpty(DropboxTokens.ApplicationSecret))
        throw new NullReferenceException("ApplicationSecret not set.");

      var settings = _configuration.GetOrCreateAs<DropboxSettings>();

      if (settings.Token == null)
        throw new UnauthorizedAccessException("Authorization info was corrupted. Please reauthorize in Settings -> SignIn.");

      try
      {
        string key = settings.Token.Key;
        string secret = settings.Token.Secret;
        var client = new DropNetClient(DropboxTokens.ApplicationKey, DropboxTokens.ApplicationSecret, key, secret);
        client.UseSandbox = true;

        MetaData metaData = await client.UploadFileTask(FOLDER_TO_SAVE, fullName, data); // upload data

        if (metaData == null || string.IsNullOrEmpty(metaData.Path))
          return null;

        ShareResponse share = null;

        for (int i = 0; i < repetitions; i++)
        {
          share = await client.GetShareTask(metaData.Path, shortUrl && !useDirectlink);

          if (share != null)
            break;

          await Task.Delay(waitTime);
        }

        if (share == null || string.IsNullOrEmpty(share.Url))
          return null;

        if (useDirectlink)
          return share.Url.Replace("www.dropbox.com", "dl.dropboxusercontent.com").Replace("?dl=0", "");

        return share.Url;
      }
      catch (DropboxException ex)
      {
        throw ProcessDropboxException(ex);
      }
    }

    #region Settings

    public ImageSaveFormat CurrentImageFormat
    {
      get
      {
        var settings = _configuration.GetOrCreateAs<DropboxSettings>();
        return settings.CurrentImageFormat;
      }
      internal set
      {
        var settings = _configuration.GetOrCreateAs<DropboxSettings>();
        settings.CurrentImageFormat = value;
        _configuration.SaveAs(settings);
      }
    }

    #endregion

    #region Properties

    public Image Logo
    {
      get { return LocalResources.dropbox; }
    }

    public string Name
    {
      get { return LocalResources.DropboxStorage_Name; }
    }

    public string Description
    {
      get { return LocalResources.DropboxStorage_Description; }
    }

    public IEnumerable<ImageSaveFormat> SupportedFormats
    {
      get
      {
        return new[]
        {
          ImageSaveFormat.Png,
          ImageSaveFormat.Jpg,
          ImageSaveFormat.Jpeg,
          ImageSaveFormat.Gif
        };
      }
    }

    #endregion
  }
}